package jp.co.nes.awssupport.business.utility;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.Format;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;

import com.amazonaws.regions.Regions;

import jp.co.nes.awssupport.business.bean.BillingCsvBean;
import jp.co.nes.awssupport.business.constant.AwsProductName;
import jp.co.nes.awssupport.business.constant.LineItemType;
import jp.co.nes.awssupport.business.constant.Term;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.utility.DateUtil;

/**
 * 請求CSVユーティリティです。
 */
public class BillingCsvUtil {

    /** LineItemTypeを格納するマップ(キー名：LineItemTypeが請求CSVに出力される際の文字列) */
    private static Map<String, LineItemType> lineItemTypeMap = new HashMap<>();

    /** Termを格納するマップ(キー名：Termが請求CSVに出力される際の文字列) */
    private static Map<String, Term> termMap = new HashMap<>();

    /**
     * 初期化処理です。
     */
    static {
        lineItemTypeMap.put("Credit", LineItemType.CREDIT);
        lineItemTypeMap.put("DiscountedUsage", LineItemType.DISCOUNTED_USAGE);
        lineItemTypeMap.put("Fee", LineItemType.FEE);
        lineItemTypeMap.put("RIFee", LineItemType.RI_FEE);
        lineItemTypeMap.put("Tax", LineItemType.TAX);
        lineItemTypeMap.put("Usage", LineItemType.USAGE);
        lineItemTypeMap.put("Refund", LineItemType.REFUND);

        termMap.put("OnDemand", Term.ON_DEMAND);
        termMap.put("Reserved", Term.RESERVED);
    }

    /**
     * デフォルトコンストラクタです。<br>
     * 本クラスはインスタンスを生成しません。
     */
    private BillingCsvUtil() {
    }

    /**
     * 引数の文字列をLineItemTypeに変換して返却します。
     *
     * @param value 文字列
     * @return LineItemType
     */
    public static LineItemType toLineItemTypeFromBillingCsv(String value) {
        LineItemType result = lineItemTypeMap.get(value);
        if (result == null) {
            throw new CommonException("value is wrong(value=" + value + ")");
        }
        return result;
    }

    /**
     * 引数の文字列をTermに変換して返却します。
     *
     * @param value 文字列
     * @return Term
     */
    public static Term toTermFromBillingCsv(String value) {
        Term result = termMap.get(value);
        if (result == null) {
            throw new CommonException("value is wrong(value=" + value + ")");
        }
        return result;
    }

    /**
     * リザーブド購入(前払い料金) or リザーブド購入(定額月額料金)かどうかを返却します。
     *
     * @param csvBean 請求CSV
     * @return true: リザーブド購入(前払い料金) or リザーブド購入(定額月額料金)である,
     *         false: リザーブド購入(前払い料金) or リザーブド購入(定額月額料金)でない。
     */
    public static boolean isReservedUpfrontOrMonthlyFixed(BillingCsvBean csvBean) {
        LineItemType lineItemType = csvBean.getLineItemType();
        String reservationARN = csvBean.getReservationARN();
        if (lineItemType == LineItemType.FEE || lineItemType == LineItemType.RI_FEE) {
            if (!StringUtils.isEmpty(reservationARN)) {
                return true;
            }
        }
        return false;
    }

    /**
     * リザーブド購入(前払い料金)かどうかを返却します。
     *
     * @param csvBean 請求CSV
     * @return true: リザーブド購入(前払い料金)である, false: リザーブド購入(前払い料金)でない。
     */
    public static boolean isReservedUpfront(BillingCsvBean csvBean) {
        LineItemType lineItemType = csvBean.getLineItemType();
        String reservationARN = csvBean.getReservationARN();
        if (lineItemType == LineItemType.FEE && !StringUtils.isEmpty(reservationARN)) {
            return true;
        }
        return false;
    }

    /**
     * リザーブド購入(定額月額料金)かどうかを返却します。
     *
     * @param csvBean 請求CSV
     * @return true: リザーブド購入(定額月額料金)である, false: リザーブド購入(定額月額料金)でない。
     */
    public static boolean isReservedMonthlyFixed(BillingCsvBean csvBean) {
        LineItemType lineItemType = csvBean.getLineItemType();
        String reservationARN = csvBean.getReservationARN();
        if (lineItemType == LineItemType.RI_FEE && !StringUtils.isEmpty(reservationARN)) {
            return true;
        }
        return false;
    }

    /**
     * リザーブドインスタンスの従量料金かどうかを返却します。
     *
     * @param csvBean 請求CSV
     * @return true: リザーブドインスタンスの従量料金である, false: リザーブドインスタンスの従量料金でない。
     */
    public static boolean isReservedMetered(BillingCsvBean csvBean) {
        LineItemType lineItemType = csvBean.getLineItemType();
        Term term = csvBean.getTerm();
        String reservationARN = csvBean.getReservationARN();
        if (lineItemType == LineItemType.DISCOUNTED_USAGE) {
            if (term == Term.RESERVED && !StringUtils.isEmpty(reservationARN)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 融通リザーブドかどうかを返却します。
     *
     * @param csvBean 請求CSV
     * @return true: 融通リザーブドである, false: 融通リザーブドでない。
     */
    public static boolean isBlendedReserved(BillingCsvBean csvBean) {
        if (!isReservedMetered(csvBean)) {
            return false;
        }
        String linkedAccountId = csvBean.getLinkedAccountId();
        String reservationARN = csvBean.getReservationARN();
        if (!StringUtils.isEmpty(linkedAccountId)) {
            String linkedAccountIdFromReservationARN = getLinkedAccountIdFromReservationARN(reservationARN);
            if (StringUtils.isEmpty(linkedAccountIdFromReservationARN)) {
                // 連結アカウントIDを取得できなかった場合
                throw new CommonException("linkedAccountId get fail from ReservationARN");
            }
            if (!linkedAccountId.equals(linkedAccountIdFromReservationARN)) {
                // reservationARNから取得したIDと請求CSVの連結アカウントが異なる場合融通と判断する。
                return true;
            }
        } else {
            throw new CommonException("linkedAccountId is null or empty");
        }
        return false;
    }

    /**
     * スポットかどうかを返却します。
     *
     * @param csvBean 請求CSV
     * @return true: スポットである, false: スポットでない。
     */
    public static boolean isSpot(BillingCsvBean csvBean) {
        LineItemType lineItemType = csvBean.getLineItemType();
        String productName = csvBean.getProductName();
        String usageType = csvBean.getUsageType();
        String reservationARN = csvBean.getReservationARN();
        if (lineItemType == LineItemType.FEE && StringUtils.isEmpty(reservationARN)) {
            return true;
        }

        // TODO 暫定対応
        if (lineItemType == LineItemType.FEE && !StringUtils.isEmpty(reservationARN)) {
            if (AwsProductNameUtil.isEquals(AwsProductName.EC2, productName)) {
                String id = getIdFromReservationARN(productName, reservationARN);
                if (!StringUtils.isEmpty(id) && id.startsWith("riex-")) {
                    return true;
                }
            }
        }

        if (lineItemType == LineItemType.REFUND) {
            return true;
        }

        if (lineItemType == LineItemType.USAGE) {
            if (AwsProductNameUtil.isEquals(AwsProductName.EC2, productName) && usageType != null
                    && usageType.indexOf("-SpotUsage") >= 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * 日時集計結果に出力するかどうかを返却します。
     *
     * @param csvBean 請求CSV
     * @return true: 出力する, false: 出力しない。
     */
    public static boolean isDailyOutput(BillingCsvBean csvBean) {
        LineItemType lineItemType = csvBean.getLineItemType();
        if (lineItemType == null) {
            return false;
        }
        if (isSpot(csvBean)) {
            // TODO Spotも出力する。
            return false;
        }
        return true;
    }

    /**
     * 無効な請求CSVかどうかを返却します。
     *
     * @param csvBean 請求CSV
     * @return true: 無効である, false: 無効でない。
     */
    public static boolean isInvalid(BillingCsvBean csvBean) {
        String billingEntity = csvBean.getBillingEntity();
        if (StringUtils.isEmpty(billingEntity) || !billingEntity.toLowerCase().equals("aws")) {
            return true;
        }

        if (LineItemType.CREDIT == csvBean.getLineItemType()) {
            return true;
        }

        if (LineItemType.TAX == csvBean.getLineItemType()) {
            return true;
        }

        String linkedAccountId = csvBean.getLinkedAccountId();
        if (StringUtils.isEmpty(linkedAccountId)) {
            return true;
        }
        if (linkedAccountId.equals(csvBean.getPayerAccountId())) {
            return true;
        }

        if (isSpot(csvBean)) {
            if (LineItemType.FEE == csvBean.getLineItemType()) {
                BigDecimal unblendedCost = csvBean.getUnblendedCost();
                if (unblendedCost == null || unblendedCost.compareTo(BigDecimal.ZERO) < 0) {
                    // マイナスの場合は無効
                    return true;
                }
            }
        } else {
            if (isReservedUpfrontOrMonthlyFixed(csvBean)) {
                BigDecimal usageQuantity = csvBean.getUsageQuantity();
                if (usageQuantity == null || usageQuantity.compareTo(BigDecimal.ZERO) <= 0) {
                    // null or 0以下の場合(0も含めて、マイナスの場合)
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * リザーブドインスタンスARNからリザーブドインスタンスのIDを抽出し、返却します。<br>
     * リザーブドインスタンスARNから抽出できなかった場合はnullを返却します。<br>
     * なお、製品名がEC2, ElastiCache, RedShift, DynamoDB, RDS, Elasticsearch以外だった場合もnullを返却します。
     *
     * @param reservationARN リザーブドインスタンスARN
     * @return リザーブドインスタンスのID
     */
    public static String getIdFromReservationARN(String productName, String reservationARN) {
        String id = null;
        if (AwsProductNameUtil.isEquals(AwsProductName.EC2, productName)
                || AwsProductNameUtil.isEquals(AwsProductName.REDSHIFT, productName)
                || AwsProductNameUtil.isEquals(AwsProductName.DYNAMO_DB, productName)
                || AwsProductNameUtil.isEquals(AwsProductName.ELASTIC_SEARCH_SERVICE, productName)) {
            int lastIndex = reservationARN.lastIndexOf("/");
            if (lastIndex >= 0) {
                id = reservationARN.substring(lastIndex + 1);
            }
        } else if (AwsProductNameUtil.isEquals(AwsProductName.ELASTI_CACHE, productName)
                || AwsProductNameUtil.isEquals(AwsProductName.RDS, productName)) {
            String[] splitValues = reservationARN.split(":");
            if (splitValues != null && splitValues.length == 7) {
                return splitValues[6];
            }
        }
        return id;
    }

    /**
     * リザーブドインスタンスARNからリージョンを抽出し、返却します。<br>
     * リザーブドインスタンスARNから抽出できなかった場合はnullを返却します。
     *
     * @param reservationARN リザーブドインスタンスARN
     * @return リージョン
     */
    public static Regions getRegionsFromReservationARN(String reservationARN) {
        Regions regions = null;
        if (!StringUtils.isEmpty(reservationARN)) {
            String[] split = reservationARN.split(":");
            if (split.length >= 4) {
                regions = Regions.fromName(split[3]);
            }
        }
        return regions;
    }

    /**
     * リザーブドインスタンスARNから連結アカウントIDを抽出し、返却します。<br>
     * リザーブドインスタンスARNから抽出できなかった場合はnullを返却します。
     *
     * @param reservationARN リザーブドインスタンスARN
     * @return 連結アカウントID
     */
    private static String getLinkedAccountIdFromReservationARN(String reservationARN) {
        String account = null;
        if (!StringUtils.isEmpty(reservationARN)) {
            // 呼び出し元で、reservationARNが空でない場合に呼び出されるので、reservationARNが空の場合は存在しない。
            String[] split = reservationARN.split(":");
            if (split.length >= 5) {
                account = split[4];
            }
        }
        return account;
    }

    /**
     * 請求CSVリストの内容を連結アカウントID, 製品名, 使用タイプ, 使用開始日時、使用終了日時、リソースIDでグループ化し、
     * 連結アカウントID, 製品名, 使用タイプ, 使用開始日時、使用終了日時、リソースIDでソートでソートします。<br>
     * なお、 引き渡されたリストが、null or 空リストの場合は、引き渡されたリストを返却します。
     *
     * @param csvList 請求CSVリスト
     * @return グループ化し、ソートした結果の請求CSVリスト
     */
    public static List<BillingCsvBean> grouping(List<BillingCsvBean> csvList) {
        if (csvList == null || csvList.isEmpty()) {
            // 引き渡されたリストが、null or 空リストの場合は、引き渡されたリストを返却する。
            return csvList;
        }

        Format f = new DecimalFormat("0000000000.000000000000");

        // TODO
        // 連結アカウントID, 製品名, 使用タイプ, 使用開始日時、使用終了日時、リソースID、レートでグループ化し、マップに設定する。
        // なお、マップのキーは、「連結アカウントID + 製品名 + 使用タイプ + 使用開始日時 + 使用終了日時 + リソースID + レート」、
        // 値は、「連結アカウントID, 製品名, 使用タイプ, 使用開始日時、使用終了日時、リソースID、レート」が同じ請求CSVのリストになる。
        Map<String, List<BillingCsvBean>> groupMap = csvList.stream().collect(Collectors.groupingBy(bean -> {
            StringBuilder keyBuilder = new StringBuilder();
            keyBuilder.append(bean.getLinkedAccountId());
            keyBuilder.append(bean.getProductName());
            keyBuilder.append(bean.getUsageType());

            ZonedDateTime usageStartDate = bean.getUsageStartDate();
            ZonedDateTime usageEndDate = bean.getUsageEndDate();
            if (usageStartDate == null) {
                throw new CommonException("usageStartDate is null");
            }
            if (usageEndDate == null) {
                throw new CommonException("usageEndDate is null");
            }

            keyBuilder.append(DateUtil.toString(usageStartDate));
            keyBuilder.append(DateUtil.toString(usageEndDate));
            keyBuilder.append(bean.getResourceId());
            keyBuilder.append(f.format(bean.getRate()));
            return keyBuilder.toString();
        }));

        // グループ化されたマップから値に格納されているリストを取得し、その内容を1つの請求CSVに集約して、
        // 集約した請求CSVをリストに登録する。
        List<BillingCsvBean> resultCsvList = new ArrayList<>();
        Collection<List<BillingCsvBean>> groupValueCollection = groupMap.values();
        for (List<BillingCsvBean> groupList : groupValueCollection) {
            if (groupList.isEmpty()) {
                continue;
            }

            BillingCsvBean groupBean = new BillingCsvBean();

            // 連結アカウントID, 製品名, 使用タイプ, 使用開始日時、使用終了日時、リソースID、レートは、リストの先頭を使用する。
            groupBean.setLinkedAccountId(groupList.get(0).getLinkedAccountId());
            groupBean.setProductName(groupList.get(0).getProductName());
            groupBean.setUsageType(groupList.get(0).getUsageType());
            groupBean.setUsageStartDate(groupList.get(0).getUsageStartDate());
            groupBean.setUsageEndDate(groupList.get(0).getUsageEndDate());
            groupBean.setResourceId(groupList.get(0).getResourceId());
            groupBean.setRate(groupList.get(0).getRate());

            // TODO
            // Nameタグ、説明、単位は、nullを除外し、maxで取得した値を使用する。
            // →通常、連結アカウントID, 製品名, 使用タイプ, 使用開始日時、使用終了日時、リソースID、レートで
            //   グループ化されたものについては、Nameタグ、説明、単位に同じ値が設定されているはずであり、maxで取得した値で問題ない。
            //   なお、他のもについては、そもそも使用されないので、どのような値でも問題なし。
            groupList.stream()
                    .map(BillingCsvBean::getNameTag)
                    .filter(nameTag -> nameTag != null)
                    .max(Comparator.naturalOrder())
                    .ifPresent(nameTag -> groupBean.setNameTag(nameTag));
            groupList.stream()
                    .map(BillingCsvBean::getDescription)
                    .filter(description -> description != null)
                    .max(Comparator.naturalOrder())
                    .ifPresent(description -> groupBean.setDescription(description));
            groupList.stream()
                    .map(BillingCsvBean::getUnit)
                    .filter(unit -> unit != null)
                    .max(Comparator.naturalOrder())
                    .ifPresent(unit -> groupBean.setUnit(unit));

            // 使用量はnullを除外したものを合算し、その結果を使用する。
            groupList.stream()
                    .map(BillingCsvBean::getUsageQuantity)
                    .filter(usageQuantity -> usageQuantity != null)
                    .reduce(BigDecimal::add)
                    .ifPresent(usageQuantity -> groupBean.setUsageQuantity(usageQuantity));

            resultCsvList.add(groupBean);
        }
        return resultCsvList;
    }
}
